package jmine.tec.persist.hibernate.initializer;

import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Map.Entry;

import jmine.tec.persist.hibernate.SessionImplementorReference;
import jmine.tec.persist.hibernate.proxy.LazyCollectionProxy;
import jmine.tec.persist.hibernate.proxy.LazyPojoProxy;
import jmine.tec.persist.hibernate.proxy.SerializableLazyProxy;
import jmine.tec.utils.spring.BeanReference;
import jmine.tec.utils.spring.SpringBeanReference;

import org.hibernate.EntityMode;
import org.hibernate.SessionFactory;
import org.hibernate.collection.AbstractPersistentCollection;
import org.hibernate.collection.PersistentCollection;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.type.Type;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

/**
 * Inicializa entidades de persistência encapsulando atributos que carregam de forma "lazy" para que não percam a sessão do hibernate (mesmo
 * após terem sida serializadas).
 * 
 * @author wellington.santos
 * @created Jun 4, 2012
 */
@Scope(BeanDefinition.SCOPE_SINGLETON)
@Component
public class LazyProxyInitializer implements ProxyInitializer {

    private SessionFactory sessionFactory;

    /**
     * Cache de uma referência para {@link SessionImplementor}.
     */
    private BeanReference<SessionImplementor> sessionReference;

    /**
     * Cache de uma referência para {@link LazyProxyInitializer}.
     */
    private BeanReference<LazyProxyInitializer> initializerReference;

    /**
     * {@inheritDoc}
     */
    public final void initialize(Object value) {
        this.initialize(new IdentityHashMap<Object, Object>(), value, null, null);
    }

    /**
     * Inicializa um objeto.
     * 
     * @param done mapa de objetos já processados
     * @param value objeto a inicializar
     * @param parent entidade ou coleção onde o objeto está contido
     * @param propertyKey nome ou índice que permite acessar o valor na entidade
     */
    protected void initialize(Map<Object, Object> done, Object value, Object parent, Object propertyKey) {
        if (value != null) {
            LazyResolverEnum.forObject(value).resolve(value);
            this.initializeMembers(done, value);
        }
    }

    /**
     * Encapsula um objeto se ele for "lazy" e ainda não tenha sido carregado.
     *  
     * @param value objeto a inicializar
     * @param <T> tipo do valor
     * @return o próprio objeto inicializado ou uma proxy caso este seja um "lazy" ainda não inicializado
     */
    public final <T> T wrap(T value) {
        return this.wrap(new IdentityHashMap<Object, Object>(), value, null, null, null);
    }

    /**
     * Encapsula um objeto se ele for "lazy" e ainda não tenha sido carregado.
     * 
     * @param done mapa de objetos já processados
     * @param value objeto a encapsular
     * @param parent entidade ou coleção onde o objeto está contido
     * @param propertyKey nome ou índice que permite acessar o valor na entidade
     * @param propertyType tipo da propriedade onde o objeto está contido
     * @param <T> tipo do valor
     * @return o próprio objeto inicializado ou uma proxy caso este seja um "lazy" ainda não inicializado
     */
    @SuppressWarnings("unchecked")
    protected <T> T wrap(Map<Object, Object> done, T value, Object parent, Object propertyKey, Type propertyType) {
        if (value != null) {
            if (!(value instanceof SerializableLazyProxy<?>)) {
                if (value instanceof HibernateProxy) {
                    HibernateProxy target = (HibernateProxy) value;
                    if (target.getHibernateLazyInitializer().isUninitialized()) {
                        return (T) new LazyPojoProxy(this.getInitializerReference(), this.getSessionReference(), target).getProxy();
                    } else {
                        this.initializeMembers(done, target.getHibernateLazyInitializer().getImplementation());
                    }
                } else if (value instanceof PersistentCollection) {
                    AbstractPersistentCollection target = (AbstractPersistentCollection) value;
                    if (!target.wasInitialized()) {
                        return (T) new LazyCollectionProxy(this.getInitializerReference(), this.getSessionReference(), target).getProxy();
                    } else {
                        this.initializeMembers(done, target.getValue());
                    }
                } else {
                    this.initializeMembers(done, value);
                }
            }
        }
        return value;
    }

    /**
     * Inicializa os atributos de uma entidade ou elementos de uma coleção.
     * 
     * @param done mapa de objetos já processados
     * @param target objeto que terá seus atributos inicializados
     */
    protected void initializeMembers(Map<Object, Object> done, Object target) {
        if (done.put(target, target) == null) {
            if (target instanceof Map<?, ?>) {
                this.initialize(done, ((Map<?, ?>) target).entrySet(), target, "entrySet");
            } else if (target instanceof Collection<?>) {
                int index = 0;
                for (Object value : (Collection<?>) target) {
                    this.initialize(done, value, target, index++);
                }
            } else if (target instanceof Object[]) {
                Object[] vector = (Object[]) target;
                for (int index = vector.length - 1; index >= 0; index--) {
                    this.initialize(done, vector[index], target, index);
                }
            } else if (target instanceof Entry<?, ?>) {
                Entry<?, ?> value = (Entry<?, ?>) target;
                Object key = value.getKey();
                this.initialize(done, key, target, "key");
                this.initialize(done, value.getValue(), target, "value");
            } else {
                ClassMetadata classMetadata = this.sessionFactory.getClassMetadata(target.getClass());
                if (classMetadata != null) {
                    Type[] propertyTypes = classMetadata.getPropertyTypes();
                    String[] propertyNames = classMetadata.getPropertyNames();
                    Object[] propertyValues = classMetadata.getPropertyValues(target, EntityMode.POJO);
                    boolean modified = false;
                    for (int i = 0, c = propertyValues.length; i < c; i++) {
                        Object value = propertyValues[i];
                        Object wrappedValue = this.wrap(done, value, target, propertyNames[i], propertyTypes[i]);
                        if (value != wrappedValue) {
                            modified = true;
                            propertyValues[i] = wrappedValue;
                        }
                    }
                    if (modified) {
                        classMetadata.setPropertyValues(target, propertyValues, EntityMode.POJO);
                    }
                }
            }
        }
    }

    /**
     * @return uma referência para a sessão do hibernate
     */
    protected BeanReference<? extends SessionImplementor> getSessionReference() {
        if (this.sessionReference == null) {
            this.sessionReference = new SessionImplementorReference(SpringBeanReference.forBean(this.sessionFactory));
        }
        return this.sessionReference;
    }

    /**
     * @return uma referência para este {@link ProxyInitializer} 
     */
    protected BeanReference<? extends ProxyInitializer> getInitializerReference() {
        if (this.initializerReference == null) {
            this.initializerReference = SpringBeanReference.forBean(this);
        }
        return this.initializerReference;
    }

    /**
     * @return a fábrica de sessões do hibernate
     */
    public final SessionFactory getSessionFactory() {
        return this.sessionFactory;
    }

    /**
     * @param sessionFactory a fábrica de sessões do hibernate
     */
    @Autowired
    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

}
